-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Keycloak modules retry request on authentication error, support refresh token parameter #9494
Conversation
…not yet working [8857]
…ith retry logic [8857]
… credentials [8857]
…n, valid refresh token [8857]
…without username/pass [8857]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hi @armkeh thanks for your contribution!
Couple of comments on the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your contribution! Please add a changelog fragment. Thanks.
…h token not provided (ansible-collections#8857)
…rn exception on failure (ansible-collections#8857)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple of comments more.
def _request(self, url, method, data=None): | ||
def make_request_catching_401(): | ||
try: | ||
return open_url(url, method=method, data=data, | ||
http_agent=self.http_agent, headers=self.restheaders, | ||
timeout=self.connection_timeout, | ||
validate_certs=self.validate_certs) | ||
except HTTPError as e: | ||
if e.code != 401: | ||
raise e | ||
return e | ||
|
||
r = make_request_catching_401() | ||
if not isinstance(r, Exception): | ||
return r | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Little late in the game for that, I know, but I believe this would be leaner as:
def _request(self, url, method, data=None): | |
def make_request_catching_401(): | |
try: | |
return open_url(url, method=method, data=data, | |
http_agent=self.http_agent, headers=self.restheaders, | |
timeout=self.connection_timeout, | |
validate_certs=self.validate_certs) | |
except HTTPError as e: | |
if e.code != 401: | |
raise e | |
return e | |
r = make_request_catching_401() | |
if not isinstance(r, Exception): | |
return r | |
def _request(self, url, method, data=None): | |
class Unauthorized(Exception): | |
pass | |
def make_request_catching_401(): | |
try: | |
return open_url(url, method=method, data=data, | |
http_agent=self.http_agent, headers=self.restheaders, | |
timeout=self.connection_timeout, | |
validate_certs=self.validate_certs) | |
except HTTPError as e: | |
if e.code != 401: | |
raise e | |
raise Unauthorized() | |
try: | |
return make_request_catching_401() | |
exception Unauthorized: | |
pass | |
except Exception: | |
raise |
and so on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this, however we need to keep the original 401 exception in case no re-auth options are available and it needs to be re-raised. (Thanks for catching the mistake where it was being returned and not raised in your other comment.)
r = make_request_catching_401() | ||
|
||
return r | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This last call to make_request_catching_401()
is not being verified. If, for whatever reason, the HTTP request inside that call returns a 401, that Exception object will be returned (not raised) and no error handling will be performed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, good catch; I've reworked the retry wrappers to make the flow a little clearer, removing early returns, and made sure to throw the exception if that's the final result.
…ble-collections#8857) Add test to verify this behaviour works.
…thentication failures (ansible-collections#8857)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one possible improvement and a comment, other than that LGTM
try: | ||
r = json.loads(to_native(open_url(auth_url, method='POST', | ||
validate_certs=validate_certs, http_agent=http_agent, timeout=connection_timeout, | ||
data=urlencode(payload)).read())) | ||
except ValueError as e: | ||
raise KeycloakError( | ||
'API returned invalid JSON when trying to obtain access token from %s: %s' | ||
% (auth_url, str(e))) | ||
except Exception as e: | ||
raise KeycloakError('Could not obtain access token from %s: %s' | ||
% (auth_url, str(e)), authError=e) | ||
|
||
try: | ||
token = r['access_token'] | ||
except KeyError: | ||
raise KeycloakError( | ||
'Could not obtain access token from %s' % auth_url) | ||
|
||
return token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be simpler to go:
try: | |
r = json.loads(to_native(open_url(auth_url, method='POST', | |
validate_certs=validate_certs, http_agent=http_agent, timeout=connection_timeout, | |
data=urlencode(payload)).read())) | |
except ValueError as e: | |
raise KeycloakError( | |
'API returned invalid JSON when trying to obtain access token from %s: %s' | |
% (auth_url, str(e))) | |
except Exception as e: | |
raise KeycloakError('Could not obtain access token from %s: %s' | |
% (auth_url, str(e)), authError=e) | |
try: | |
token = r['access_token'] | |
except KeyError: | |
raise KeycloakError( | |
'Could not obtain access token from %s' % auth_url) | |
return token | |
try: | |
r = json.loads(to_native(open_url(auth_url, method='POST', | |
validate_certs=validate_certs, http_agent=http_agent, timeout=connection_timeout, | |
data=urlencode(payload)).read())) | |
return r['access_token'] | |
except ValueError as e: | |
raise KeycloakError( | |
'API returned invalid JSON when trying to obtain access token from %s: %s' | |
% (auth_url, str(e))) | |
except KeyError: | |
raise KeycloakError( | |
'Could not obtain access token from %s' % auth_url) | |
except Exception as e: | |
raise KeycloakError('Could not obtain access token from %s: %s' | |
% (auth_url, str(e)), authError=e) |
or remove the except KeyError
block entirely - the raised error after that is almost the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Rather than exclude the KeyError
catch, I made it's message clearer about the problem (response did not include access_token
).
except HTTPError as e: | ||
if e.code != 401: | ||
raise e | ||
return e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still bugs me on principle - exceptions are meant to be raised not returned :-) - but let's not hold this PR back because of it. I'll make a note to myself to return to this later and see if I can make it any better.
LGTM as well. |
…name/password (ansible-collections#8857) To emphasize their difference from the `get_token` function, which either gets the token from the module params *or* makes a request for it.
… error during auth request (ansible-collections#8857)
Thank you all for the feedback! I've addressed the comments (except @russoz's regarding the helper function returning an exception, for which a neat refactor isn't obvious), and added docstrings to the functions I've added or significantly reworked; so outside of any other feedback, I think this is ready to go. One note, there are some integration tests failing for the Keycloak modules; I confirmed those same tests fail on the main branch currently. I'm planning to try and repair them in a separate PR. |
Backport to stable-10: 💚 backport PR created✅ Backport PR branch: Backported as #9631 🤖 @patchback |
…sh token parameter (#9494) * feat: begin refactor to support refresh token in keycloak modules * chore: add start of tests for shared token usage * feat: progress towards supporting refresh token; token introspection not yet working [8857] * chore: reset to main branch previous state; a different approach is needed [8857] * feat: add request methods to keycloak class, which will be expanded with retry logic [8857] * feat: all requests to keycloak use request methods instead of open_url [8857] * fix: data argument is optional in keycloak request methods [8857] * feat: add integration test for keycloak module authentication methods [8857] * chore: refactor get token logic to separate logic using username/pass credentials [8857] * chore: refactor token request logic further to isolate request logic [8857] * chore: fix minor lint issues [8857] * test: add (currently failing) test for request with invalid auth token, valid refresh token [8857] * chore: allow realm to be provided to role module with refresh_token, without username/pass [8857] * feat: add retry logic to requests in keycloak module utils [8857] * chore: rename keycloak module fail_open_url method to fail_request [8857] * chore: update all keycloak modules to support refresh token param [8857] * chore: add refresh_token param to keycloak doc_fragments [8857] * chore: restore dependency between auth_realm and auth_username,auth_password params [8857] * chore: rearrange module param checks to reduce future pr size [8857] * chore: remove extra comma [8857] * chore: update version added for refresh token param [8857] * chore: add changelog fragment [8857] * chore: re-add fail_open_url to keycloak module utils for backward compatability [8857] * fix: do not make a new request to keycloak without reauth when refresh token not provided (#8857) * fix: only make final auth attempt if username/pass provided, and return exception on failure (#8857) * fix: make re-auth and retry code more consistent, ensure final exceptions are thrown (#8857) * test: fix arguments for invalid token, valid refresh token test (#8857) * feat: catch invalid refresh token errors during re-auth attempt (#8857) Add test to verify this behaviour works. * test: improve test coverage, including some unhappy path tests for authentication failures (#8857) * chore: store auth errors from token request in backwards compatible way (#8857) * fix: ensure method is still specified for all requests (#8857) * chore: simplify token request logic (#8857) * chore: rename functions to request tokens using refresh token or username/password (#8857) To emphasize their difference from the `get_token` function, which either gets the token from the module params *or* makes a request for it. * doc: add docstrings for new or significantly modified functions (#8857) * test: repair unit test following change to exception message upon key error during auth request (#8857) (cherry picked from commit af01182)
…t on authentication error, support refresh token parameter (#9631) Keycloak modules retry request on authentication error, support refresh token parameter (#9494) * feat: begin refactor to support refresh token in keycloak modules * chore: add start of tests for shared token usage * feat: progress towards supporting refresh token; token introspection not yet working [8857] * chore: reset to main branch previous state; a different approach is needed [8857] * feat: add request methods to keycloak class, which will be expanded with retry logic [8857] * feat: all requests to keycloak use request methods instead of open_url [8857] * fix: data argument is optional in keycloak request methods [8857] * feat: add integration test for keycloak module authentication methods [8857] * chore: refactor get token logic to separate logic using username/pass credentials [8857] * chore: refactor token request logic further to isolate request logic [8857] * chore: fix minor lint issues [8857] * test: add (currently failing) test for request with invalid auth token, valid refresh token [8857] * chore: allow realm to be provided to role module with refresh_token, without username/pass [8857] * feat: add retry logic to requests in keycloak module utils [8857] * chore: rename keycloak module fail_open_url method to fail_request [8857] * chore: update all keycloak modules to support refresh token param [8857] * chore: add refresh_token param to keycloak doc_fragments [8857] * chore: restore dependency between auth_realm and auth_username,auth_password params [8857] * chore: rearrange module param checks to reduce future pr size [8857] * chore: remove extra comma [8857] * chore: update version added for refresh token param [8857] * chore: add changelog fragment [8857] * chore: re-add fail_open_url to keycloak module utils for backward compatability [8857] * fix: do not make a new request to keycloak without reauth when refresh token not provided (#8857) * fix: only make final auth attempt if username/pass provided, and return exception on failure (#8857) * fix: make re-auth and retry code more consistent, ensure final exceptions are thrown (#8857) * test: fix arguments for invalid token, valid refresh token test (#8857) * feat: catch invalid refresh token errors during re-auth attempt (#8857) Add test to verify this behaviour works. * test: improve test coverage, including some unhappy path tests for authentication failures (#8857) * chore: store auth errors from token request in backwards compatible way (#8857) * fix: ensure method is still specified for all requests (#8857) * chore: simplify token request logic (#8857) * chore: rename functions to request tokens using refresh token or username/password (#8857) To emphasize their difference from the `get_token` function, which either gets the token from the module params *or* makes a request for it. * doc: add docstrings for new or significantly modified functions (#8857) * test: repair unit test following change to exception message upon key error during auth request (#8857) (cherry picked from commit af01182) Co-authored-by: Mark Armstrong <[email protected]>
11.2.0 Major Changes community.general - keycloak_* modules - ``refresh_token`` parameter added. When multiple authentication parameters are provided (``token``, ``refresh_token``, and ``auth_username``/``auth_password``), modules will now automatically retry requests upon authentication errors (401), using in order the token, refresh token, and username/password (ansible-collections/community.general#9494). community.vmware - vmware_dvswitch_pvlans - The VLAN ID type has been updated to be handled as an integer (ansible-collections/community.vmware#2267). dellemc.openmanage - omevv_firmware - This module allows to update firmware of the single host and single cluster. fortinet.fortios - Support check_mode on all the configuration modules. google.cloud - google_cloud_ops_agents - role submodule removed because it prevents the collection from passing sanity and lint tests grafana.grafana - Ability to set custom directory path for *.alloy config files - Fix 'dict object' has no attribute 'path' when running with --check - Update grafana template - add loki bloom support - grafana.ini yaml syntax
SUMMARY
Fixes #8857.
Wraps all requests to Keycloak in the Keycloak modules (
keycloak_authentication
,keycloak_authz_authorization_scope
,keycloak_authz_custom_policy
, etc.) with retry logic to make use of a newrefresh_token
module parameter.This improves the user experience when using Keycloak modules with the
auth_token
parameter; previously if that token expired during playbook execution, subsequent tasks would fail. Now they "fall back" to using therefresh_token
, or, if it is not provided or is expired itself, to using theauth_username
andauth_password
.ISSUE TYPE
COMPONENT NAME
keycloak